3
3
.
.
1
1
.
.
6
6
@
@
O
O
b
b
s
s
e
e
r
r
v
v
e
e
d
d
O
O
b
b
j
j
e
e
c
c
t
t
I
I
n
n
f
f
o
o
[
[
R
R
]
]
[
[
R
R
]
]
This tutorial contains more complex analysis on how to use: ObservableObject, @Published, @ObservedObject.
If you create instance of a struct and then use it in different Views, each View will get its own copy of that instance.
This is because Instances of a struct are given by value and not by reference (struct is value type and not reference type).
This means that changes done to one Instance/Copy will not be propagated to other Instances/Copies.
In other words these Instances/Copies will not be in synch - each will have its own data.
So if you want multiple Views to share the same data then instead of structs you need to use Classes.
Instances of Class are given by reference, so every View gets a reference that points to the same Instance of the Class.
So changes made by one View will be visible to other Views.
Content
Using Struct (Struct Instance is given to Views by creating additional copies. Views work on different data.)
Using Class (Class Instance is given to Views by creating the same reference. Views work on same data.)
Using Class Listener (With @ObservedObject changes in Class Instance are immediately reflected on the Screen.)
U
U
s
s
i
i
n
n
g
g
S
S
t
t
r
r
u
u
c
c
t
t
[
[
R
R
]
]
In this example in the ContentView we create an Instance of a User struct called user.
Then we use this instance as input parameter to LoginView1 and LoginView2.
What happens is that each of these Views gets a different copy of the user Instance (so now we have three copies).
This is why changes made to user Instance inside one of these Views are not visible in the other View.
For instance if we change Username in the LoginView1 this change will not be visible in LoginView2.
This is because these Views are working on a different copies of user Instance.
We can use Buttons in each View to print current values of user Instance and it will be different for each View.
ContentView.swift
import SwiftUI
//==========================================================================
// User
//==========================================================================
struct User {
var userName : String
var password : String
var emailAddress : String
}
//==========================================================================
// ContentView
//==========================================================================
struct ContentView : View {
var user = User(userName: "John", password: "mypassword", emailAddress: "john@gmail.com")
var body : some View {
HStack(spacing: 20) {
LoginView1(user: user).border(Color.red , width: 3)
LoginView2(user: user).border(Color.blue, width: 3)
}
}
}
//==========================================================================
// LoginView1
//==========================================================================
struct LoginView1 : View {
@State var user : User
var body : some View {
VStack {
TextField("Enter Username" , text: $user.userName ).padding()
TextField("Enter Password" , text: $user.password ).padding()
TextField("Enter Email Address", text: $user.emailAddress).padding()
Button("PRINT NAME") { print(self.user.userName) }
}
}
}
//==========================================================================
// LoginView2
//==========================================================================
struct LoginView2 : View {
@State var user : User
var body : some View {
VStack {
TextField("Enter Username" , text: $user.userName ).padding()
TextField("Enter Password" , text: $user.password ).padding()
TextField("Enter Email Address", text: $user.emailAddress).padding()
Button("PRINT NAME") { print(self.user.userName) }
}
}
}
Views start with the same data But changes to Name are not synched
U
U
s
s
i
i
n
n
g
g
C
C
l
l
a
a
s
s
s
s
[
[
R
R
]
]
To allow different Views to share data we need to use Class instead of Struct.
This is because Classes are given by reference while Structs are given by value (by making copies).
So when you call another View and give it an Instance of a Class you are giving it a reference to that Instance.
So all Views will get the reference to the same Instance which means that they are all working on the same shared data.
So in this example if User changes Username in any of the Views this change is automatically propagated to the Class
Instance. We can confirm that by pressing PRINT NAME buttons in both of the Views which will always print the same
value because both Views are taking the value from the same instance.
But change will not be propagated to other elements of the View or to the other View. This is because @State wrapper
doesn't propagate changes if it is related to Class Instance (it works only for variables and structs). To propagate changes
from Class Instance to Views we need to replace @State with @ObservedObject (and do some other changes to Class as
shown in next example).
Changes from the previous example are highlighted in yellow
replacing keyword struct with class
adding init() (because Classes don't have default initializers)
ContentView.swift
import SwiftUI
class User {
var userName : String
var password : String
var emailAddress : String
init(userName: String, password: String, emailAddress: String) {
self.userName = userName
self.password = password
self.emailAddress = emailAddress
}
}
//==========================================================================
// ContentView
//==========================================================================
struct ContentView : View {
var newUser = User(userName: "NewJohn", password: "New123", emailAddress: "Newjohn@gmail.com")
var body : some View {
HStack(spacing: 20) {
LoginView1(user: newUser).border(Color.red , width: 3)
LoginView2(user: newUser).border(Color.blue, width: 3)
}
}
}
//==========================================================================
// LoginView1
//==========================================================================
struct LoginView1 : View {
@State var user : User
var body : some View {
VStack {
TextField("Enter Username" , text: $user.userName ).padding()
TextField("Enter Password" , text: $user.password ).padding()
TextField("Enter Email Address", text: $user.emailAddress).padding()
Button("PRINT NAME") { print(self.user.userName) }
Text(user.userName) //It will not get updated
}
}
}
//==========================================================================
// LoginView2
//==========================================================================
struct LoginView2 : View {
@State var user : User
var body : some View {
VStack {
TextField("Enter Username" , text: $user.userName ).padding()
TextField("Enter Password" , text: $user.password ).padding()
TextField("Enter Email Address", text: $user.emailAddress).padding()
Button("PRINT NAME") { print(self.user.userName) }
Text(user.userName) //It will not get updated
}
}
}
Views start with the same data Changes in one are reflected inside the other
Debug Console
John123
John123
U
U
s
s
i
i
n
n
g
g
C
C
l
l
a
a
s
s
s
s
L
L
i
i
s
s
t
t
e
e
n
n
e
e
r
r
[
[
R
R
]
]
[
[
R
R
]
]
[
[
R
R
]
]
To allow Class to inform View that its Properties have changed and that View should be redrawn we have to use
ObservableObject Protocol that must be implemented by the Class
@Published wrapper around each Property that should trigger View to redraw
@ObservedObject wrapper around Class Instance inside the View (so that View takes notice of changes)
Now whenever Class Instance Property changes it will update all parts of the existing View and the other View.
SwiftUI replaced
ObservableObject was previously BindableObject
@ObservedObject was previously @ObjectBinding
objectWillChange was previously willChange
ContentView.swift
import SwiftUI
//==========================================================================
// Class: User
//==========================================================================
class User : ObservableObject {
@Published var userName : String
@Published var password : String
@Published var emailAddress : String
init(userName: String, password: String, emailAddress: String) {
self.userName = userName
self.password = password
self.emailAddress = emailAddress
}
}
//==========================================================================
// ContentView
//==========================================================================
struct ContentView : View {
var newUser = User(userName: "John", password: "mypassword", emailAddress: "john@gmail.com")
var body : some View {
HStack(spacing: 20) {
LoginView1(user: newUser).border(Color.red , width: 3)
LoginView2(user: newUser).border(Color.blue, width: 3)
}
}
}
//==========================================================================
// LoginView1
//==========================================================================
struct LoginView1 : View {
@ObservedObject var user : User
var body : some View {
VStack {
TextField("Enter Username" , text: $user.userName ).padding()
TextField("Enter Password" , text: $user.password ).padding()
TextField("Enter Email Address", text: $user.emailAddress).padding()
}
}
}
//==========================================================================
// LoginView2
//==========================================================================
struct LoginView2 : View {
@ObservedObject var user : User
var body : some View {
VStack {
TextField("Enter Username" , text: $user.userName ).padding()
TextField("Enter Password" , text: $user.password ).padding()
TextField("Enter Email Address", text: $user.emailAddress).padding()
}
}
}
Both Views hold reference to the same Class Instance Changes in are reflected in the other View as we type